Skip to content

Reference page fixes: sticky headings, parameters, tmux links and alignment - #157

Merged
tony merged 84 commits into
mainfrom
sticky-member-headings
Oct 19, 2026
Merged

tony merged 84 commits into
mainfrom
sticky-member-headings

Conversation

@tony

@tony tony commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes from the 2026-10-18 review of the reference pages, on one branch so they share one CI run, one preview and one deploy. #158 to #163 were folded in here; each closed PR keeps its full discussion and evidence. Later fixes from the same review land on this branch too.

Sticky member and group headings

Long member lists scrolled the Members heading and the active group heading out of view, so a reader lost track of which group a member belonged to. Both headings now stay stuck below the site header and section row, and release at the end of the Members section. This was P0 6, held back from #156 until review found the problems below.

What a reader gets

  • The Members bar and the current group bar stay visible while scrolling a long member list.
  • A link or typed hash to a group lands at the start of that group, not its end.
  • Keyboard focus is never fully hidden under the bars (WCAG 2.2 SC 2.4.11). A focused bar returns to its place in the page and stays above the strip that covers the gap.
  • Heading IDs, permalinks, heading levels, print, no-JavaScript and reduced-motion behaviour are unchanged.

Fixed after the first review

  • Focus could sit fully under the bars when tabbing backward through a group.
  • In Firefox, a first click to an earlier group could land at the group's end, and a repeated same-hash click overshot.
  • A typed hash for a group above the viewport landed at the group's end in every engine.
  • A strip of scrolled text showed between the section row and the bars, and the strip's cover then painted over a focused Members bar.

Verification

  • node site/scripts/check-dev.mjs --api-sticky runs focus, link, repeated-link and typed-hash scenarios in Chromium, Firefox and WebKit at 1800 and 390px. Its controls fail in each engine without the focus clearance, without the typed-hash handling, or with an unpositioned Members bar.
  • site/test/api-page-contents.test.ts: 69 tests, including the new cases that fail on the unfixed sources.
  • An independent re-review approved it with nits.

Known gaps

  • Firefox scrolls a focused control only when none of it is visible, so a control can stay partly under a bar. The gate requires "not entirely hidden" there, which is the WCAG AA rule.
  • Without the Navigation API, a typed hash still lands at the group's end; clicks are handled either way.
  • CI installs Chromium only, so the Firefox-only cases run locally.

Keyword-argument literals stay unlinked (was #158)

A prose literal shaped like a keyword argument (name: or name: value) stays plain in HTML and Markdown exports. The Ruby Server reference had linked cancel: to Cancellation#cancel, clients: true to Snapshot#clients and where: to Selection#where. A bare name such as cancel still links. Test: inline-reference-appearance.test.ts, which fails without the rule.

Every parameter listed in every port (was #159)

Python, Swift, Ruby and Lua listed only the parameters a doc comment described, so Ruby Server.start showed no Parameters section. Every port now lists each declared parameter with its type, and each parameter name in the signature links to its entry.

  • Python lists the implementation's parameters, not each @overload stub's.
  • An undocumented leading self (or Python cls, or Lua self) is the receiver and is neither listed nor linked.

tmux CLI Manual and C source reference in the sidebar (was #160)

The tmux sidebar ends with CLI Manual and C source reference for the selected tmux version, and the overview cards use the same names as the documentation picker. Test: documentation-navigation.test.ts.

tmux commands, options, hooks and format variables badged and previewed (was #161)

tmux names in guides, port prose and API doc comments render like API mentions: a kind badge ($ command, # format, o option, h hook), a hover card from the pinned tmux manual, and a link to the manual entry. tmuxEntityHref(kind, name, version) in tmux-links.ts resolves all four kinds.

  • On the Rust concepts page, list-clients, session_attached and destroy-unattached now carry badges.
  • session_name still links to the Rust API field, because library symbols outrank tmux names.
  • Markdown exports keep plain links.

Tests: tmux-mentions.test.ts (42) and link-preview.test.ts. Screenshots are in #161; delete the tmux-mention-badges-screenshots branch after review.

Declaration controls level with the first signature line (was #162)

The badges, [source] and permalink beside a declaration are now level with the centre of the signature's first line, also when the signature wraps. On TS Server.sessions they sat 4.5px high, because the formatted signature's padding was counted. The fix is in site/src/styles/api.css; the vendored gp-sphinx CSS is untouched.

Gate: checkDeclarationControlAlignment in check-native-layout.mjs, run by check-dev.mjs --reference-layout. It measures 48 headers across TS, Go, Python, Julia, C#, Kotlin, Scala and the Rust MCP page at 1440, 768, 500 and 390px. It fails on main (-4.50px) and passes here (worst 0.70px, on MCP type headings).

Table row permalinks trail the cell text (was #163)

A prose table row's now sits right after the first cell's last text line, hidden until the row is hovered, focused or targeted. Markdown table cells share a 0.75rem inline inset, so the first column no longer touches the table edge.

Gate: checkRowPermalinks in check-dev-sample.mjs checks the Rust, tmux and Ruby CLI tables in light and dark at 1440, 768 and 390px. It fails on main ("permalink starts 39.8px after its text") and passes here.

Type parameters documented and linked

Model in Selection.where(criteria: WhereOf<Model>): Selection<Model> was plain text, because the TypeScript extractor never recorded the type parameters of interfaces, classes or aliases. Extractors now record them, and one linker binds every occurrence to the declaring page's new Type parameters section. Member pages say which declaration owns the parameter, and Markdown exports carry the section.

  • Recorded: names, bounds, defaults and docs for TS, Rust, Go, Java and C#; Kotlin and Scala add variance. Python covers PEP 695 lists, Generic[...] bases and TypeVar bounds. C++ covers templates, concepts and \tparam. Julia and Ruby are read from their recorded parameters.
  • Gaps: the Swift extractor is updated but the Swift model is not regenerated, because swift build fails on this host (missing libxml2.so.2). F# models carry no generics, so 'T stays unlinked.
  • Data: regenerating the older committed models also reorders some keys in Rust and Java, and picks up one C# doc comment and one Java record accessor. Only the type-parameter fields are merged into cxx.json, because the local Doxygen differs from the one that produced it.
  • Tests: 27 new per-port tests fail on main. check-dev.mjs --type-parameters follows the links to the declaring anchor on ts, py, rs, go and java, and fails on the old ts.json.

tmux key sequences render as keyboard keys

The tmux manual and guides now render key sequences such as C-b c as <kbd> caps (<kbd><kbd>C-b</kbd> <kbd>c</kbd></kbd>) instead of code.

  • What counts as a key: detection follows tmux's own grammar in key-string.c (site/src/lib/tmux-keys.ts). Modifiers C-, M- and S-, ^X, and the key names the manual spells (Enter, Escape, BSpace, F1–F12, the keypad and so on) count.
  • Where: modified keys convert anywhere. Bare key names convert only in key contexts: key tables, the default-bindings list, the bind-key/send-keys descriptions, or "press"/"the ‘c’ key". Format strings, option values, command names and paths stay code.
  • Manual data: converted at render time, so the committed manual data is unchanged. Markdown twins and exports keep backticks.
  • Counts: 329 key literals in the 3.2a manual and 258 in 3.7c.
  • Tests: tmux-keys.test.ts (95 cases) and the manual and plugin tests. check-dev.mjs --tmux-keys asserts the bind-key page's DOM.

Kind badges: no dotted underline, centred, one gap; cards are not previews

  • Underline: a decoration on an anchor reaches every in-flow child, so a leading badge inherited the preview's dotted underline (F# concepts M/C). The name now draws the underline and the badge draws none. Non-preview badge links draw their solid underline on the name only.
  • Centring and gap: badges centre on the text line (align-self: center) with a .25em gap everywhere. The gap had been anywhere from .2em to .67em, and badges sat 1.5px to 8px low in body text, headings and tables. Nav and sidebar rows were already centred and keep their 4px gap.
  • Cards: the Python workspace "Runtime support" card links into reference/, so the preview script marked it and painted a dotted underline across the whole card on hover. Links that wrap blocks, and .doc-card, are no longer preview candidates.
  • Gates:
    • checkKindBadgeLinks asserts one dotted underline on the name, the badge centre within 1px and a gap of about .25em.
    • checkBlockLinksUndecorated hovers every block link on the workspace, port and MCP overview pages, with a prose page as the inline control.
    • Both fail on main. The unit tests inline-reference-appearance, link-preview-paint and mcp-reference also fail on the old source.

Member bodies: padding alone sets the inset

On the Go Server.ConfigFile page a member body had 20px above its first line and 12px below its last. Elsewhere it was 12px above and 20px below. The cause was a top or bottom margin on the first or last child (a paragraph, a field list or an example) added to the body's 12px padding. One rule in site/src/styles/api.css replaces the earlier narrow fixes: edge children of dl.py > dd, down to three levels, lose their outer margin. A field list that opens the body also loses its divider and top padding. Spacing between siblings is unchanged.

Gate: checkMemberBodyInsets (check-dev.mjs --member-bodies) measures 17 pages across Go, Julia, Ruby, Python, TS, Swift, C#, Java, PowerShell and MCP at 1440 and 390px. It requires the top inset, bottom inset and padding to agree within 1px. Before the fix it failed on 12 pages (76 body lines); now it passes on 54 bodies.

Workspace commands, options and keys link to their reference

On the Python workspace topics page, tmuxp load, tmuxp freeze and shell_command_before were plain code. Workspace names never reached a resolver, and where one did link it often went to the wrong target: freeze to a library function, search to the Python standard library, session_name to a tmux format.

  • scripts/gen-workspace-entities.mjs generates site/src/data/workspace-entities.json from each port's CLI Manual commands and option and key tables. The anchors are shared with the rendered rows. --check runs in the test loops.
  • <program> <command> (e.g. tmuxp load, tmux-workspace load) links on any page of a port that ships that program. A bare command, option or key links on user-facing workspace pages, and the page's own port and product win over library, standard-library and tmux names. Internals and API-index pages keep their library links.
  • Commands get the $ badge, options o, and keys a new : badge. MCP tool and prompt names inside longer code runs now link without a badge. Markdown exports keep plain links.
  • Across nine ports, unlinked literals went from 178 to 0. workspace-mentions.test.ts walks every workspace and MCP page through the real pipeline against an independent oracle, and fails on the reported page without the linker.
  • Known gap: ports other than Python document only session-level keys in a table, so window- and pane-level keys link to the session row.

MCP tool pages: rendered descriptions and API-style arguments

On the Python create_session page, the environment argument showed a raw :func:libtmux_mcp._utils._coerce_dict_arg. -eKEY=VALUE was unstyled, show-environment was not linked, and the Arguments list looked unlike the API pages.

  • Descriptions run through the shared prose pipeline.
    • Sphinx roles become a link when the symbol has a page, and plain code otherwise. The private _coerce_dict_arg has no page.
    • Inline literals get the code style.
    • .. warning:: renders as an aside.
    • tmux commands, options, hooks and formats link with badges.
    • A symbols: false flag stops an argument named like a library attribute from linking to it.
  • Arguments use the API parameter-list style: name? = default (type) #.
    • Nullable unions fold into the type line instead of separate Variant blocks, and Record<string, T> shows as a map.
    • A closed object reads "No other arguments are accepted."
    • All 79 element ids on create_session are unchanged, and folded variant anchors stay as aliases.
  • Counts across ten ports:
    • Sphinx roles: 36 to 0.
    • Unlinked tmux names: 77 to 0.
    • Unstyled literals: 618 to 0.
  • Tests: mcp-descriptions.outer.test.ts sweeps every port's MCP descriptions and fails on main's rendering, with clean controls.

tmux examples build up to their scripts

The native tmux example pages opened with the complete script. Each page now starts with a short concept paragraph and works through numbered steps, one tmux command or idea at a time, each with a small block and links to the manual. "The complete script" comes last and is tested unchanged.

  • Step counts: attach and send keys has 11, capture pane output has 8, and workspace from file has 8.
  • Step blocks are anchored excerpts of the tested script. A fence names excerpt="<title>" or file="<path>" plus from and to text anchors. The build fails if the from anchor matches no line or more than one, or if to is missing or above from. Markdown exports resolve the same excerpts. 29 tests cover this, including mutation checks on each guard.
  • Not yet: the check that fails on a hand-typed block no test covers, and the Go pilot.

Indentation kept in Javadoc examples

The Java TmuxEnvironment example rendered flush left. The Javadoc extractor stripped the * margin and every space after it, so the model stored try (…) {\nSession mine = …. It now strips only the margin, so relative indentation inside <pre>{@code} survives, and the Java model is regenerated.

  • Six Java symbols had flattened examples. Only their examples[].code changed in the regenerated model.
  • The Doxygen extractor had the same bug: it collapsed a \\code listing onto one line. No committed C++ comment uses \\code yet.
  • scripts/check-example-indentation.mjs compares each comment-derived example in every committed model against its source, and runs in test-all.sh. A negative script plants the Java shape.

tmux options, hooks and formats catalog

/tmux/<version>/catalog/ adds options/, hooks/ and formats/ indexes and a page per entry, in the C reference's layout, with "Used by" lists and hover cards. tmux 3.2a has 104 options, 61 hooks and 200 formats; 3.7c has 153, 68 and 258.

  • tmuxEntityHref now sends options, hooks and formats to catalog pages. Commands stay in the CLI Manual.
  • Across guides and API docs there are 1,791 option, hook and format mentions. 1,677 now reach a catalog page, 18 reach a library symbol, and 96 stay plain: keyword arguments, parameter names and doctest output.
  • scripts/gen-tmux-catalog.mjs --check and its negative script run in the test loops. A crawl of 1,335 catalog pages found no broken links.

Verification of the combined branch

  • The 13 test files these changes touch pass together on the combined tree: 446 tests.
  • pnpm run lint exits 0. The two remaining warnings are in files this branch does not change.
  • Each folded PR's own browser gates passed on its branch, as recorded in that PR. CI on this branch runs them together.

tony added 4 commits October 18, 2026 01:20
why: Long member lists scroll the Members heading and the active
subgroup heading out of view, so a reader loses which group a member
belongs to. The section row already clears the site header; the member
headings need the same measured clearance without moving their IDs,
permalinks or heading levels.

what:
- Wrap an owner's Members or Fields section in `.api-members` and stack
  its real h2 and group h3 below the measured header and section row.
  Both release at the section end, before inherited and related content.
- Derive offsets, scroll margins and activation from the measured
  heading heights, so wrapped and enlarged text keep clearance. Stay in
  flow when the viewport cannot keep a reading area beside the bars.
- Return a link request for a stuck owner or group heading to its source
  boundary, including a repeated hash. The anchor handler and the
  browser keep startup arrival, history traversal, typed hashes,
  modified clicks, new tabs and cross-page links.
- Keep the bars in flow until the page is revealed, so the anchor
  handler's startup scroll is not skewed by a sticky heading in Firefox.
- Return a keyboard-focused heading to its source box, but keep it
  sticky under a pointer press so the permalink click still lands.
- Tint the opaque bar for the arrival cue instead of replacing its
  surface, including under reduced motion.
- Confine sticky rules to screen media; leave scalar, single-group and
  empty owners without added headings.
- Cover grouped, single, scalar and empty owners, release, wrapped
  headings, link requests, history, startup, the focus and arrival
  styles and the pending state in the api-page-contents tests.
why: A sticky box is aligned where it is stuck, not where its group
starts. A link or typed hash for a group above the viewport landed at
the end of that group in every engine, and Firefox did the same for a
first link click when a frame rendered before the URL changed. The
scroll padding counted only the header and section row, so Shift+Tab
left focused links entirely under the Members and group bars. A strip
of scrolled text also showed between the section row and the bars.

what:
- Hold the targeted Members or group heading in flow while a link click
  or a same-document fragment navigation scrolls, so the browser and
  the anchor handler align its source box. The Navigation API
  `navigate` event starts the hold before the browser scrolls a typed
  or scripted hash; a click in the layout starts it in browsers
  without that API. Animation frames end the hold, and every
  navigation signal restarts it.
- Give focusable controls in the owner section a scroll margin equal to
  the measured bars above them: the Members bar, plus the control's own
  group bar. A heading permalink takes only the bars above its heading.
- Cover the gap between the section row and the bars with the page
  background, using a sticky pseudo-element that adds no height.
- Test the hold, its restart and release, the Navigation API entry
  point, the focus clearance and the cover in api-page-contents.test.ts.

Verified: with the Swift Server page at 1800x1000, backward Shift+Tab
left 2 of 14 stops under a bar in Chromium and Firefox and 7 in WebKit;
none are now. A typed hash for the first group landed at scrollY 3609,
3723 and 3608 (Chromium, Firefox, WebKit), with the group box ending at
207, 94 and 208; it lands at 2677, 2678 and 2678 with the group
starting at the reading line. A rail click to the first group from a
later one ended at the group end in Firefox in 3 of 24 runs, and in 18
of 24 on a loaded machine; it ended there in 0 of 24 runs, twice, now.
why: The api-page-contents tests mock geometry in a headless DOM, so
none of the focus, landing or typed hash failures of the sticky bars
could fail them, and each engine aligns a sticky heading differently.

what:
- Add checkApiStickyHeadings. On the Swift Server page, at desktop and
  phone widths, it tabs backward through a stuck group, clicks a link
  to an earlier group once normally and once with the main thread busy,
  clicks a heading's link again from mid-group and from the group end,
  and types a hash for a group above the viewport. Each arrival must
  sit at the reading line, at the start of its group, uncovered, and
  reached by one scroll that never paints the group end.
- Add checkApiStickyHeadingsFail. It removes the focus clearance and
  the Navigation API from a page and requires the check to reject, so
  the check cannot pass without them.
- Run both in the default browser matrix, and in Chromium, Firefox and
  WebKit with `check-dev.mjs --api-sticky`. Document the mode in
  CONTRIBUTING.md.

Verified: against the sticky commit alone, check-dev.mjs --api-sticky
fails in Chromium and Firefox at "backward tab stop 11 is not entirely
hidden by the sticky bars" and in WebKit at "backward tab stop 9 starts
below the sticky bars". Run per scenario, a typed hash fails in all
three engines. In Firefox a link on a busy page lands with the heading
at 48 under the header (scrollY 3723), and a repeated link overshoots
to 3915 instead of 3687. With the fix the command passes in all three
engines, and the scenarios passed in three consecutive runs each.
why: Keyboard focus, the arrival hold and pending fonts put a member bar
back in flow with position: static. The sticky cover that closes the
gap under the section row is positioned, so it painted over the top of
the static Members bar, its permalink and its focus outline.

what:
- Hold in-flow bars with position: relative and top: auto, so their
  z-index keeps them above the cover
- Check in real browsers that a focused Members permalink is not covered,
  with a control that fails when the bar drops to static
@tony
tony deployed to docs-preview October 18, 2026 08:03 — with GitHub Actions Active
tony added 6 commits October 18, 2026 06:23
why: Prose code literals link every name that resolves to one member.
A keyword argument names a parameter of the call being described, so
Ruby's `cancel:` linked to Cancellation#cancel, `clients: true` to
Snapshot#clients, and `action:` to an unrelated Workspace member.

what:
- Add SymbolIndex.linkLiteral, which leaves `name:` and `name: value`
  plain and links other literals as before
- Use it for HTML prose, and skip keyword arguments in Markdown export
- Test that keyword arguments stay plain while the bare name still links
why: Python, Swift, Ruby and Lua listed only the parameters a doc comment
described, so a Ruby Server.start page showed no Parameters section for
executable:, config:, timeout: and cancel:, while C# listed its
parameters with their types whether documented or not.

what:
- List each declared parameter with its type in every port
- Link every parameter name in the signature to its entry
- For Python, list the implementation's parameters, not each @overload
  stub's, and leave out an undocumented self or cls receiver; leave out
  Lua's self receiver too
- Update the tests that pinned documented-only lists
why: The documentation picker named the tmux CLI Manual and C source
reference, but the tmux sidebar listed only Guides, Concepts, Examples
and Topics, and the overview cards called them Manual and Reference.

what:
- Add CLI Manual and C source reference links to the tmux sidebar,
  after its sections and before the notices
- Title the overview cards CLI Manual and C source reference
- Test the sidebar links for a pinned tmux version
…s, formats

why: A tmux command such as `list-clients` rendered as a plain link,
with no kind badge or hover card, while the API names beside it carried
both. `session_attached` and `destroy-unattached`, which the tmux manual
documents, were not linked at all.

what:
- Read options, hooks and format variables from the pinned manual per
  release, anchored on the same headings the full manual page renders
- Resolve every command, option, hook and format-variable link through
  tmuxEntityHref, so the destination of every kind changes in one place
- Link option and hook names written in code, and format variables that
  are a whole code span or sit inside a #{...} expression; names the
  release does not document, URI templates and calls stay plain
- Keep library symbols ahead of options, hooks and format variables in
  HTML, prose Markdown and symbol Markdown, so `pane_id` still reaches a
  Rust field where the page documents one
- Render each lone mention with the API kind badge (command $, option o,
  hook h, format #) and add the four kinds to the badge palette in the
  light, system dark and explicit dark themes; several names in one code
  run stay links without badges, and Markdown keeps plain links
- Serve preview cards for command pages and for each manual entry by
  anchor, with an "Open manual" action, and make manual links eligible
  for hover previews outside the command index and heading permalinks
- Load the highlighter only when the manual is highlighted, so link
  resolution no longer imports it
- Test the three mention kinds, the hover-card data and unknown names,
  and accept manual entry anchors in the native prose destination check
why: The badges, [source] link and permalink beside a declaration were
top-aligned with its code. A formatted native signature pads its code by
0.25rem, so on a one-line declaration such as TypeScript's
Server.sessions the controls sat 4.5px above the line's center. Badges
aligned by their own baseline sat 0.5px high on every other header, and
a heading with a taller line box drifted by more.

what:
- Align the row's first baselines for native headers at every width and
  for Python and MCP headers above the 36rem container breakpoint, so
  controls follow line one of a wrapped signature and ignore the code's
  padding; center the badge slot on the line the text controls share
- Leave the vendored column layout, header height and wrapping as they
  were; a control group on a row of its own is not moved
- Add checkDeclarationControlAlignment to check-native-layout.mjs. It
  loads TypeScript, Go, Python, Julia, C#, Kotlin, Scala and MCP pages at
  1440, 768, 500 and 390px and requires every control beside the code to
  sit within 1px of the center of the first signature line, and
  controls that wrap onto their own rows to be level within each row.
  check-dev.mjs runs it with the signature layouts and the sample loop
  checks the API probe header
- Replace the top-edge assertion in checkNativeSignatureGeometry with a
  center assertion, and replay the observed hosted Scala geometry as the
  rejected case

Largest control offset from the first line's center, origin/main then
this change, measured in Chromium against the dev server:
- TypeScript, Go, Julia and C# formatted methods: 4.50px, then 0.00px at
  1440 and 768px; TypeScript client.toString at 390px: 4.00px, then 0.00px
- Python method and class, Kotlin type: 0.50px, then 0.00px
- MCP tool header: 0.50px, then 0.00px; its type definition headings,
  whose 600-weight face makes a 25px line: 1.80px, then 0.70px
- Header heights, code boxes and the stacked layouts are unchanged
why: A prose table row's permalink sat at its first cell's right edge,
far from the label it links, and appeared inconsistently with the
section permalink. Typography also zeroes the outer padding of the
first and last columns, so labels touched the table edge and the row's
hover background.

what:
- Keep the row permalink at the end of its first cell's last line, out
  of flow so it neither widens the column nor wraps onto its own line
- Hide it until the row is hovered, focused or targeted, with the
  section permalink's theme colors
- Give markdown table cells a 0.75rem inline inset, first and last
  columns included; component tables keep their own padding
- Add checkRowPermalinks to check-dev-sample.mjs and run it from the
  sample and full check-dev flows. Rust, tmux and Ruby tables at 1440,
  768 and 390 px in light and dark must put the permalink within 0.5em
  of the last text line, share one inset across columns, stay quiet at
  rest, and appear legibly on hover, focus and target
tony added 12 commits October 18, 2026 06:34
why: `Selection.where(criteria: WhereOf<Model>): Selection<Model>` showed
`Model` as plain text. The TypeScript extractor never recorded the
parameters of an interface, class or alias, so the linker had nothing to
bind, and every port that did record them left the name unlinked or
resolved it against a global of the same name.

what:
- Add `TypeParameter` (name, modifier, kind, bound, default, doc) and
  `typeParameters` on symbols and signatures. `typeParametersOf` returns
  the stored list or reads `typeParams` apart per language, so models
  extracted earlier keep working.
- Record container and alias parameters in the TypeScript, Rust, Go,
  Java and C# specs, fold Rust and C# `where` bounds into the parameter
  they restrict, and read `@typeParam`, `@template`, `@tparam`, Javadoc
  `@param <T>` and C# `<typeparam>` onto the parameter.
- Extract parameters for Kotlin and Scala (bounds, variance, `where`),
  Python (PEP 695 lists, `Generic[...]` bases, module TypeVars and the
  variables a function binds), C++ (class and member template lists,
  `\tparam`) and Swift (generic parameters and constraints from the
  symbol graph).
- Bind names in one place, `SymbolIndex.typeParameterBindings`: the
  signature, then the enclosing declarations, honouring Java's static
  boundary, Rust impl self types and Go receivers by position. Signature
  and prose links share it, so a bound name links to the anchor
  `<owner>.type-parameter.<name>` on its declaring page.
- `<typeparamref>` becomes a `typeparam` reference that never falls back
  to a global lookup.
- Guard against the C# grammar reading a base type's arguments as a
  parameter list after a parse error.
- `inheritedTypeParameters` lists the enclosing parameters a member's
  types use, from the same links.
why: A reader of `Selection.where` could not tell what `Model` was. Type
parameters were neither documented nor linked, in any port.

what:
- List a symbol's own parameters under "Type parameters", each with a
  stable anchor, its bound or constraint, default and documentation. A
  member page lists the enclosing parameters its signature uses, linked
  to the owner's anchor and naming the declaring type.
- Link each occurrence in native, formatted-layout and unformatted
  signatures, in bounds, and in prose code literals. Layout ranges that
  keep a bound name out of the global lookup now link it to its
  declaration.
- Add the section to the on-page contents and to the Markdown export,
  built from the same bindings as the HTML.
- Give type-parameter links `api-type-parameter-name` and a title naming
  the declaring type, so parameter-name checks keep counting parameters.
- Add `check-dev.mjs --type-parameters`: it follows the links on member
  pages in five ports to the owner's anchor, and compares the Markdown
  twin. The default browser run includes it.
- Update the tests that expected a bound name to be unlinked.
why: The extractors now record type parameters, and the page tests that
compare a fresh extraction with the committed model require the two to
agree.

what:
- Regenerate ts, rs, go, csharp, java, kotlin, scala and py from the
  revisions each model already records, with `gen-api-model.mjs`. The
  existing models were produced by older extractor versions, so a few
  unrelated fields move: signature key order in Rust and Java, one C#
  doc comment and one Java record accessor.
- Merge only the type-parameter fields into cxx.json: the local Doxygen
  is 1.14 and rewrites inline code in other doc comments.
- Leave swift.json: `swift build` does not run here (missing
  libxml2.so.2). Ruby, Julia, Lua, PowerShell and F# come from native
  artifacts or sources that were not rebuilt; Ruby and Julia are read
  from their recorded `typeParams`.
why: The manual page for bind-key showed `C-b c` and `c` as code, the
same as `new-window` or a flag. A reader could not tell the key to press
from the command it runs, and the default binding tables listed keys as
bold text.

what:
- Add tmux-keys.ts with the key grammar from tmux's key-string.c and a
  policy for how much context a literal needs: modified keys qualify on
  their spelling, key names in the manual, and every key in a key table,
  the default binding list or the bind-key, unbind-key, send-keys,
  send-prefix and list-keys descriptions
- Add markTmuxManualKeys, which turns those literals into kbd, nests one
  kbd per key inside a kbd for a sequence such as C-b c, keeps permalink
  ids, and drops mandoc's quotation marks around a cap
- Leave format strings, option values, command names and bare letters
  elsewhere in the manual as code
- Draw a key cap in the prose theme from the surface and edge tokens, so
  light and dark share one rule; the layer is utilities because the
  typography plugin's own kbd rule sits there
- Test the grammar and the contexts, the bind-key sequence, the key
  tables of both pinned versions, command pages against the full manual,
  and the plain-text Markdown export
why: Guides name keys such as C-c, Enter and Up in inline code, and the
attaching guide spelled the detach binding as bold Ctrl-b and d. They
read as code beside the manual's key caps.

what:
- Add a rehype plugin, before the API linker, that marks inline code as
  kbd when tmux-keys.ts says it names a key: modified notation, a list
  that holds one, or a name beside "key" or "press"
- Keep the word Enter typed into a pane as code
- Write the detach binding as `C-b d` with the keys spelled out
- Document the convention in WRITING.md; Markdown twins keep backticks
- Test the plugin on fragments and on the key mentions of the real
  guides, topics and port pages
why: Unit tests read the transformed HTML. They do not show that the
rendered bind-key page nests the caps, keeps new-window a link, and draws
a legible cap in both colour schemes.

what:
- Add check-tmux-keys.mjs: bind-key renders C-b c as a kbd around two
  kbd, no key stays code, the command links remain; caps have an outline
  and raised edge, use the code face, fit 1440 and 390 px and reach 4.5:1
  contrast in light and dark
- Read the default bindings, the KEY BINDINGS modifiers and two guides,
  including the word Enter that stays code
- Run it in the sample and the full browser matrix, with its pages
  compiled during setup, and alone with `check-dev.mjs --tmux-keys`
why: A preview link that leads with a kind badge ([M], [C]) drew its
dotted underline under the badge letter as well as the name. A text
decoration on an ancestor reaches every in-flow descendant, and the
badge cannot cancel it, so the line has to sit on the name instead.

what:
- Put the dotted line on the link's name (.api-symbol-link__name, or
  the code name in "In other ports") and leave the anchor undecorated
  when a badge leads it
- Drop the prose underline from badge links that are not previews, and
  let their name draw it
- Wrap MCP description link names in .api-symbol-link__name, as prose
  links already do
- Check in a real browser that each badge's rendered box matches the
  same page drawn without decorations, at rest and hovered, and that the
  name has exactly one dotted underline; cover prose, headings, tables,
  MCP, API equivalents, related lists, sidebar, tree and search results
- Extend the happy-dom decoration test to the badge link shapes
why: A badge aligned to its name's baseline, so its centre sat below the
text centre and the gap to the name varied by context. Measured in
Chromium before the change: the centre was 3px low beside body text and
a code chip, 8px low in an h2, 5.5px in an h3, 1.5px in a table, 2.5px
in related cards and 1.3px in search results. The gap was .3em in prose
(4.8px to the chip, 10.8px to its text), .35em in MCP cards, .37em in
API equivalents, .6em in related cards, .67em in related contents and
.2em in search results.

what:
- Centre the badge on the link's name with align-self while the name
  keeps the baseline, so the link's height and its text do not move
- Set one .25em gap between a badge and the name in prose, MCP, related,
  equivalent and search links; navigation and sidebar rows already
  centre and keep their 4px
- Lay out search result names as a flex row so the badge and name share
  the same gap
- Move the related-card summary indent to the new badge and gap width
- Assert in the browser check that each badge centre is within 1px of its
  text centre, the gap is about .25em, and a one-line link stays within
  its line height
why: Hovering the "Runtime support" card on the workspace overview drew
the preview's dotted underline under its title and body. The card links
to reference/compatibility/, which the preview policy accepts as a
reference page, so the whole block link was marked and
a[data-link-preview] painted the line through its heading and paragraph.
Cards that link to guides were not matched, which is why only this one
showed it. A card already summarizes its destination, and only an inline
text run carries the dotted line.

what:
- Make a link that wraps blocks, or a .doc-card, ineligible for a
  preview, and remove a mark the parser added before its blocks arrived
- Check in a real browser, after hovering every block link on the
  workspace, port and MCP overview pages, that each dotted link line
  sits on an inline link without block children, with a prose page as
  the control that inline preview links keep theirs
- Cover the card and block-wrapping link shapes in the preview policy
  and decoration tests
- Test the badge check's dotted line with endsWith
why: A member body (`dl.py > dd`) is padded .75rem, but its first and
last children carried their own margins on top of that. A body holding
only "In other ports" opened 20px below its header and closed 12px
above its border; a trailing description paragraph or example did the
same at the bottom, and a leading parameter list added a divider and
padding before its label. Each case was patched on its own (the
Methods heading, then the Description and Examples headings), so the
next shape of body repeated the bug.

what:
- Trim the top margin of a body's first child, and the bottom margin of
  its last, through up to three wrappers, in one rule set in api.css
- Drop the divider and padding of a field list that opens the body
- Drop the bottom margin of the last parameter heading, which sits in
  a grid item the chain cannot reach
- Remove the narrower rules this replaces from global.css and api.css
- Add checkMemberBodyInsets to check-native-layout.mjs: it opens
  member bodies on Go, Julia, Ruby, Python, TypeScript, Swift, C#,
  Java, PowerShell and MCP pages at 1440 and 390px, finds the first
  and last painted content, and requires both insets to match the
  padding and each other within 1px
- Run it from check-dev.mjs, alone with --member-bodies
- Rewrite declaration-body-spacing.test.ts around the new rules,
  including the sibling spacing they must leave alone
…es in code

why: On /py/latest/workspace/topics/, `tmuxp load`, `tmuxp freeze` and
`shell_command_before` rendered as plain code, although the CLI Manual
and the Configuration reference have an entry for each. The prose
linker only knew library symbols, MCP names on MCP pages and tmux
names, so workspace names never reached a resolver. Where a workspace
name was linked, it went to the wrong place: `freeze` and `search` led
to a library function and the Python standard library, `session_name`
to the tmux format variable, and Ruby's `load` to its API.

what:
- Index each port's CLI Manual commands, option rows and configuration
  key rows from the workspace pages (scripts/gen-workspace-entities.mjs
  into site/src/data/workspace-entities.json), anchored on the rows the
  rendered tables serve; createRowAnchors now numbers rows for the page
  and the indexer alike
- Resolve `<program> <command>` on any page of the port that ships the
  program, and a bare command, option or key on a user-facing workspace
  page, through the mention pipeline that badges tmux names: the same
  badge markup, inline links without a badge inside a longer code run,
  plain links in Markdown exports
- Let the page's own product and port win: a workspace name outranks a
  library symbol, a standard-library name and a tmux name, while
  Internals pages and the language API index keep their library symbols
- Leave a heading, the first cell of the row that defines a name, and a
  command's own page plain rather than sending them elsewhere
- Prefer a key's entry on the page it is written on, then the first in
  reference order; a name that is both a command and a key follows its
  section, or its quotes
- Link tools and prompts named inside a longer MCP code run, and resource
  URIs on workspace pages, which a library symbol never shares
- Add a key badge (colon mark, property palette) beside the tmux kinds
- Share the linked-version rule between the prose linker and the new
  links as linkedVersionFor
- Check the index in the medium loop and the publication audit, and
  walk every workspace and MCP page of every port in a test that fails
  on any code literal naming a command, option, key, tool or resource
  that renders plain; its negative case renders the maintainer's page
  without linking and names the three reported literals

Measured on the original pipeline: 178 such literals rendered plain
across the nine ports with workspace pages, and none do now.
…iptions

why: The Python server publishes its docstrings verbatim, so
create_session printed `:func:` role text, -eKEY=VALUE and
show-environment rendered as unstyled code, and show-environment had no
link. The description renderer only knew Markdown and MCP tool names,
so every port left its tmux commands, options, hooks and format
variables unlinked, and meta descriptions carried raw backticks.

what:
- Rewrite roles and directives before Markdown renders them: a role
  becomes a link to the API reference page or MCP tool page that
  documents its target, or clean code when none exists (a private
  helper has no page and no extracted source line); :doc: links the
  upstream docs site; a warning directive becomes the site aside
- Run descriptions through the prose mention pipeline so tmux commands,
  options, hooks and format variables link through tmuxEntityHref with
  the same badges and previews as prose pages, and drop its links to the
  object being described
- Add a symbols opt-out to that pipeline: an argument named
  logical_name is not evidence for the library attribute of that name
- Give inline literals the API literal chip instead of bare code
- Use plain-text summaries for meta descriptions, search and previews
- Move the admonition kind table next to the aside it feeds
- Sweep every port's tool, prompt and resource descriptions and schema
  prose through the real renderer, failing on raw roles, directives,
  backticks, unstyled literals or unlinked tmux names, with negative
  cases on the origin/main rendering and clean controls
- Export a described tmux name as a plain Markdown link with no badge
tony added 8 commits October 18, 2026 15:14
…closure

why: The line under the page heading ("Module session") repeated the module
that the source and package disclosure already lists, and the maintainer
asked for it gone. It also carried the only visible full name and its copy
button, which Swift and PowerShell pages (no module to show) used alone.

what:
- Remove the line under the heading; the disclosure's module row is the
  same value, since both read the owner's module
- Label that row for the port (Package for Java, Kotlin, Scala and Go;
  Namespace for C#, F# and C++; Module otherwise), as the line did; name the
  distribution row Distribution where both would say Package
- Add a Qualified name row at the top of the disclosure with the copy
  button, so every page keeps its full name, including the ones with no
  module; drop the expansion control, which a wrapping row does not need
- Test the placement with negative controls, and move the browser checks
  for the Kotlin and Java package rows to the disclosure
…n the system

why: Two badge-like pieces were still on the old look: the version chip
beside the kind in the reference eyebrow (an 11px monospace grey pill on a
surface fill) and the "(struct)" or "(cmdlet)" suffix in Other languages.

what:
- Draw the version as a grey modifier chip on the legend chip's box, so the
  eyebrow row reads as one family; the three eyebrows (API reference, MCP
  reference and tmux catalog) share the markup
- Write the other-language kind as the plain kind word that member lists use,
  outside the link, instead of parentheses inside it
why: API pages showed an `overload` badge on types. The Rust page for
`session.Session` rendered "struct overload". Every extractor stamped
the modifier whenever two declarations landed on one id, and several
languages put one type or member under one id more than once:

- Rust: `struct X` or `enum X` plus its `impl X` blocks, in the same
  file (spec.ts) or in another one (the modifier unions in mergePartials)
- TypeScript: `class X` plus the `interface X` that merges into it, and
  a `static` and an instance property of one name
- C#: `X` beside `X<T>`, which share the id `Ns.X`
- Java: a field beside the accessor named after it

A type written in several parts is one type, and a field is not an
overload of its accessor. The Kotlin/Scala, C++ and Swift adapters had
the same unguarded stamp and no committed symbol reached it yet.

The badge counts per model before and after:

  rs.json      125 -> 1   (94 struct, 29 enum, 1 class removed)
  ts.json        8 -> 3   (4 class, 1 attribute removed)
  csharp.json   36 -> 34  (2 class removed)
  java.json     73 -> 72  (1 attribute removed)

what:
- Add overloads.ts: `canOverload` (a method or function with more than
  one signature), `markOverload`, `retractMisplacedOverload` and
  `misplacedOverloads`.
- Stamp through `markOverload` in the spec, Kotlin/Scala, Doxygen and
  symbol-graph adapters, and in mergePartials, which also drops an
  `overload` that a merged part carried in. A merged type keeps its
  signatures, docs and other modifiers.
- Make gen-api-model.mjs refuse to write a model that holds a misplaced
  overload, so a new adapter cannot reintroduce it quietly.
- Regenerate rs, ts, csharp and java at the revisions recorded in each
  file. The diff is only the removed modifier: each file equals its
  previous contents minus `overload` on non-callables. kotlin and scala
  regenerate unchanged from the same revision as a control.
- Test each stamping path (Rust same-file and cross-file, TypeScript
  merging and static/instance, C# arity variants and records across
  files, Java field and accessor, Kotlin expect/actual, Doxygen, symbol
  graph) beside a real overload set that keeps the modifier. The type
  cases fail against the previous adapters.
- Check all fifteen committed models: no type or lone signature carries
  `overload`, and the ports with real overload sets still have them.
why: The control shifted a modifier chip on the Rust struct page, whose only
modifier was a stamped `overload` that the model no longer carries on types.

what:
- Use the TypeScript async method page, which carries a real `async`
  modifier, for the modifier control
…in the docs sidebar

why: The MCP sidebar rule applied to the reference tree only. The
documentation sidebar lists the same tools, prompts and resources under
sections named for them, each row still carrying a "t" or a "P".

what:
- Flag the links of a protocol section whose objects share one kind as quiet
  and draw no mark on them; keep the kind on the link as its description
  for assistive technology
- Keep the marks in a section that mixes kinds, and leave the language API
  groups of the same sidebar as they were
- Hold the rule in a small shared helper, and test it on its own because no
  advertised set mixes kinds today
…nd word

why: The A to Z index named each symbol's kind in faded text, a different
treatment from the same word beside a name in a member list.

what:
- Render the kind with the plain kind word that lists use, with "type alias"
  spelled as in the cards
why: Run against the served tip, check-page-readiness failed three ways: it
looked for marks in the MCP sidebar, which now draws none under a one-kind
group; it held cards to the prose link's quarter-em gap and line-box centre,
where they now keep half an em and centre on the capitals; and its first-frame
underline probe looked for a word one box shallower than the legend chip's.

what:
- Measure the navigation tree link on an API tree, where marks remain
- Hand the MCP index card and group heading to check-badge-centring, which
  measures them from pixels with a negative control, instead of the older
  gap and centre rule
- Find the first-frame dotted underline on any label inside a linked chip
- Keep the bounded sample covering every check the gate holds, and give a
  cold reference page a minute to compile in the dev server
- Describe the new coverage in CONTRIBUTING.md
why: On master, ts, rs, ruby, lua, cxx, go and dotnet (the C# and F#
docs ports) refuse a tmux daemon older than 3.3a, including unpatched
3.3, for the core, MCP and workspace alike. Their pages still said
"tmux 3.2a or newer", so a reader on 3.2a was walked into a refusal.
Each port's master README states 3.3a as its floor.

what:
- Change "tmux 3.2a or newer" to 3.3a on the ts, rs, ruby, lua, cxx,
  go, csharp and fsharp pages: setup lines, MCP guides and examples,
  and the workspace "Runtime and configuration support" pages
- Split the shared workspace installation sentence by port: ts, rs, go,
  csharp and cxx say 3.3a; java and swift keep 3.2a
- Change the prerequisite line the complete API examples print for
  csharp, cxx, fsharp, go, lua, ruby and rust, in each *-examples.ts
  template and in the generated intro text of its API model, which is
  the substitution a regeneration at the unchanged pin makes
- Change the homepage quickstart prerequisites for ruby, lua, ts, rs
  and go

Left as written:
- java, kotlin, scala, swift, julia and powershell stay at 3.2a until
  their merges land; py, tmuxp and libtmux-mcp are unchanged
- Raw tmux shell scripts on the shared guides, concepts, topics and
  examples pages describe tmux itself
- "run on Linux with tmux 3.2a and 3.7c" on the ruby, lua and fsharp
  capture pages and the rs MCP inspect-sessions example records the
  runs the receipts in site/test/fixtures hold, which list 3.2a and
  3.7c; those need a new native run before the text can change
- The rs standard-library-core page documents a prototype pinned at an
  older revision whose own README says 3.2a
- Text extracted from port source or staged from port repositories at
  the documented pins: the TSDoc examples in ts.json, Go's Connect
  summary and the "(tmux 3.2a or later)" field notes in go.json, and
  the port-guides caches. Every API-model pin still predates the floor
  raise, so these change with a pin bump, not by hand
- tmux manual and catalog version names and tmuxp's 88-colour note
tony added 4 commits October 18, 2026 17:42
…en mark

why: A table row's permalink is built from its first cell's text, and that
text now included the letter of the hidden kind mark in front of a tmux
name. The published #tmux-version-compatibility-pane-title-changed-hook
on the tmux options and hooks page became #…-hpane-title-changed-hook,
breaking every existing link to it.

what:
- Leave aria-hidden elements out of the label a row anchor is built from
- Test a row whose first cell leads with a hidden mark
…mcp/ page

why: The Python install picker was checked on the cross-port /mcp/ page,
which now only redirects. Python's own MCP page comes from its Sphinx
build, which an assembled preview does not carry, so no built page holds
the picker any more.

what:
- Render the picker for Python and check it still offers uvx, pipx and
  pip with a cooldown control; it now runs without a built site
why: libtmux-swift's master raised its core, MCP and workspace minimum to
tmux 3.3a (libtmux/libtmux-swift#47), and the Swift pages document
master, but they still told readers tmux 3.2a was enough.

what:
- State tmux 3.3a in the Swift guide, MCP and workspace walkthroughs,
  the workspace compatibility page and the Swift API example template
  and model data
- Move Swift to the 3.3a sentence of the shared workspace installation
  page; Java keeps 3.2a until its own minimum moves
…nk ceiling

why: "List every parameter in every port" (db9c274) gave Ruby and Swift
member pages a parameter list, and each listed parameter shows its type.
Type names already plain in the signature, such as Swift's Self and
Ruby's String, Integer and untyped, now appear plain a second time in
that list, so check-type-links counted more plain names than its ceiling:
Ruby 2051 to 2910 and Swift 1959 to 2342 (CI run 39176242344).

Resolution did not get worse. On 160 sampled Swift reference pages the
signatures render the same plain names as libtmux.org at 7ffc04b, linked
type names rose from 147 to 154, and every added plain name sits in a
parameter row (capturecursor(_:_:) goes from 2 to 4 plain Self, the two
new ones under Parameters).

what:
- Record the counts CI measured as the Ruby and Swift ceilings; every
  other port's ceiling is unchanged and the ceiling still only comes down
@tony
tony deployed to docs-preview October 19, 2026 02:50 — with GitHub Actions Active
@tony
tony merged commit 4e89163 into main Oct 19, 2026
3 checks passed
@tony
tony deployed to docs-preview-cleanup October 19, 2026 03:25 — with GitHub Actions Active
tony added a commit that referenced this pull request Oct 20, 2026
why: The type-link ceiling only comes down, but a passing run printed
one total, so a ceiling raised during a wave stayed high until someone
ran --list by hand. Every name of headroom is a name that can regress
without failing the gate. The ruby and swift ceilings were raised for
#157's parameter lists before the rendering fix landed, and CI had no
output to lower them from.

what:
- After a passing check, list each port whose count is below its
  ceiling, with the recorded and measured values
- Add a negative case: a site under its ceiling passes and names the
  headroom, and the at-ceiling control prints no headroom
tony added a commit that referenced this pull request Oct 20, 2026
…red counts

why: The ceilings sat far above the plain type names each port renders,
so that many names could fall back to plain text without failing the
gate. The ruby and swift entries were raised for #157's parameter lists
before the rendering fix landed; kotlin, scala and others predate fixes
that resolved most of their names. CI on efbdddb (run 39358987283)
measured every port below its ceiling.

what:
- Set each ceiling to that run's count: py 262 -> 146, ruby 2910 -> 2109,
  lua 98 -> 74, julia 159 -> 108, ts 91 -> 18, rs 625 -> 420, go 128 -> 2,
  java 137 -> 51, kotlin 2978 -> 24, scala 3252 -> 644, fsharp 252 -> 36,
  cxx 342 -> 46, swift 2342 -> 1680
- Leave csharp at 44: the run measured 18 on the alpha.20 model, and
  this push regenerates it at alpha.22; the next run's headroom report
  measures it. powershell stays at 0

This branch was successfully deployed

2 active deployments
docs-preview-cleanup — 33b8519b Deployed Oct 19, 2026 by tony via cleanup #148
docs-preview — 33b8519b Deployed Oct 19, 2026 by tony via publish-preview / publish #869
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant